"""ROI manager: interactive define/load/save."""
from __future__ import annotations
from typing import List, Tuple
from dataclasses import dataclass
from pathlib import Path
import json
import cv2  # type: ignore


@dataclass
class ROI:
    points_norm: List[Tuple[float, float]]

    def to_pixels(self, w: int, h: int) -> List[Tuple[int, int]]:
        return [(int(x * w), int(y * h)) for x, y in self.points_norm]

    @staticmethod
    def from_pixels(points_px: List[Tuple[int, int]], w: int, h: int) -> "ROI":
        pts = [(px / max(w, 1), py / max(h, 1)) for px, py in points_px]
        return ROI(points_norm=pts)


def load_roi_json(path: str | Path) -> ROI | None:
    p = Path(path)
    if not p.exists():
        return None
    with p.open("r", encoding="utf-8") as f:
        d = json.load(f)
    pts = d.get("points_norm") or d.get("points")
    if not pts:
        return None
    return ROI(points_norm=[(float(x), float(y)) for x, y in pts])


def save_roi_json(path: str | Path, roi: ROI, frame_size: Tuple[int, int]):
    p = Path(path)
    p.parent.mkdir(parents=True, exist_ok=True)
    w, h = frame_size
    data = {
        "name": p.stem,
        "frame_size": [int(w), int(h)],
        "points_norm": [[float(x), float(y)] for x, y in roi.points_norm],
    }
    with p.open("w", encoding="utf-8") as f:
        json.dump(data, f, ensure_ascii=False, indent=2)


def interactive_define_roi(frame, window_name: str = "ROI Editor") -> ROI | None:
    h, w = frame.shape[:2]
    points: List[Tuple[int, int]] = []
    temp = frame.copy()

    def on_mouse(event, x, y, flags, param):
        nonlocal temp
        if event == cv2.EVENT_LBUTTONDOWN:
            points.append((x, y))
        elif event == cv2.EVENT_RBUTTONDOWN:
            if points:
                points.pop()
        # redraw
        temp = frame.copy()
        for i, (px, py) in enumerate(points):
            cv2.circle(temp, (px, py), 4, (0, 255, 255), -1)
            if i > 0:
                cv2.line(temp, points[i - 1], (px, py), (0, 255, 255), 2)
        if len(points) >= 3:
            cv2.polylines(temp, [np.array(points, dtype=np.int32)], isClosed=True, color=(0, 255, 0), thickness=2)

    import numpy as np  # local import to avoid heavy imports at module load
    cv2.namedWindow(window_name)
    cv2.setMouseCallback(window_name, on_mouse)
    while True:
        cv2.imshow(window_name, temp)
        k = cv2.waitKey(20) & 0xFF
        if k in (ord('s'), 13):  # 's' or Enter
            if len(points) >= 3:
                cv2.destroyWindow(window_name)
                return ROI.from_pixels(points, w, h)
        elif k == ord('r'):
            points.clear()
            temp = frame.copy()
        elif k == ord('q') or k == 27:  # q or ESC
            cv2.destroyWindow(window_name)
            return None
